﻿// <copyright file="AttackSolver.cs" company="Ecole Normale Supérieure">
// Released under GPLv2
// </copyright>
// <summary>Contains the implementation AttackSolver of IAttackSolver</summary>

namespace Sqwarea.GameLogic
{
    using System;
    using Sqwarea.GameLogic.Connexity;
    using Sqwarea.GameLogic.Extensions;
    using Sqwarea.Persistence;

    /// <summary>
    /// Class AttackSolver (implementation of IAttackSolver)
    /// </summary>
    public class AttackSolver : IAttackSolver
    {
        /// <summary>
        /// The Persistence Game Provider used
        /// </summary>
        private readonly IGameProvider _gameProvider;

        /// <summary>
        /// The Persistence Log Provider used
        /// </summary>
        private readonly ILogProvider _logProvider;

        /// <summary>
        /// The Persistence Queue Provider used
        /// </summary>
        private readonly IQueueProvider _queueProvider;

        /// <summary>
        /// The Connexity Provider used
        /// </summary>
        private readonly IConnexityProvider _connexityProvider;

        /// <summary>
        /// The user starter provider (used when a king is dead)
        /// </summary>
        private readonly IUserStarter _userStarter;

        /// <summary>
        /// The user provider (used when a king is dead)
        /// </summary>
        private readonly IUserProvider _userProvider;

        /// <summary>
        /// Initializes a new instance of the AttackSolver class.
        /// </summary>
        /// <param name="gameProvider">The Game Provider (provided by AutoFac)</param>
        /// <param name="logProvider">The Log Provider (provided by AutoFac)</param>
        /// <param name="queueProvider">The Queue Provider (provided by AutoFac)</param>
        /// <param name="connexityProvider">The Connexity Provider (provided by AutoFac)</param>
        /// <param name="userStarter">The User Starter (povider by AutoFac)</param>
        /// <param name="userProvider">Ther User Provider (provided by AutoFac)</param>
        public AttackSolver(IGameProvider gameProvider, ILogProvider logProvider, IQueueProvider queueProvider, IConnexityProvider connexityProvider, IUserStarter userStarter, IUserProvider userProvider)
        {
            if (null == gameProvider)
            {
                throw new ArgumentNullException("gameProvider", "AttackSolver");
            }

            if (null == logProvider)
            {
                throw new ArgumentNullException("logProvider", "Attacksolver");
            }

            if (null == queueProvider)
            {
                throw new ArgumentNullException("queueProvider", "AttackSolver");
            }

            if (null == connexityProvider)
            {
                throw new ArgumentNullException("connexityProvider", "AttackSolver");
            }

            if (null == userStarter)
            {
                throw new ArgumentNullException("userStarter", "AttackSolver");
            }

            if (null == userProvider)
            {
                throw new ArgumentNullException("userProvider", "AttackSolver");
            }

            this._gameProvider = gameProvider;
            this._logProvider = logProvider;
            this._queueProvider = queueProvider;
            this._connexityProvider = connexityProvider;
            this._userStarter = userStarter;
            this._userProvider = userProvider;
        }

        /// <summary>
        /// Enum used by UpdateSquareCallback to indicate the result of attack.
        /// Very useful to know how to modify different king and if connexity verification is necessary.
        /// </summary>
        private enum AttackResult
        {
            /// <summary>
            /// The attacker wins and cannot claim the square.
            /// </summary>
            AttackerWin,

            /// <summary>
            /// The attacker win and claim the square.
            /// </summary>
            AttackerWinGot,

            /// <summary>
            /// The defender won.
            /// </summary>
            DefenderWin,

            /// <summary>
            /// Not a real attack (attacker = defender), just increase troops of the king
            /// </summary>
            IncreaseTroops,

            /// <summary>
            /// Used to give a value to variable before call to function
            /// </summary>
            None
        }

        /// <summary>
        /// The "effect" of the attack on the eventual king on the square.
        /// </summary>
        private enum KingAttackResult
        {
            /// <summary>
            /// There was no king on this square.
            /// </summary>
            NoKing,

            /// <summary>
            /// There is a king and he has won.
            /// </summary>
            KingWin,

            /// <summary>
            /// There was a king but he is dead.
            /// </summary>
            KingDie
        }

        /// <summary>
        /// This method treats attacks and call connexity functions if needed (square change of king).
        /// Square troops are first removed then academy troops if possible.
        /// This is not completely atomic :
        /// * if attacked king move exactly on the same time, academy troops may fight (even if they normally can't) or cannot fight (even they normally can)
        /// * if VM break down, attack can be done twice
        /// * if square change of owner, concerned kings can be in an inconsistant state (bad SquaresNumber + bad logs)
        /// </summary>
        /// <param name="attack">attack to be treated</param>
        public void ResolveAttack(Attack attack)
        {
            AttackResult result = AttackResult.None;
            KingAttackResult kingResult = KingAttackResult.NoKing;
            int defenderAcademyTroopsLoss = 0;

            var defenderId = _gameProvider.GetSquareByPosition(attack.Destination).KingId;
            King defender;
            if (defenderId != King.NeutralKingId)
            {
                defender = _gameProvider.GetKingById(defenderId);
                defender.UpdateAcademyTroops();
            }
            else
            {
                defender = new King(0, null, null) { AcademyTroops = 0 };
            }

            // Update defender Id to put the good Id if the king is neutral
            defender.UpdateNeutralId();
            defenderId = defender.Id;

            _gameProvider.UpdateSquare(
                attack.Destination,
                s => UpdateSquareCallback(s, attack, defender, out result, out kingResult, out defenderAcademyTroopsLoss));

            // Should be called before ConnexityAnnexSquare because ConnexityAnnexSquare modify king.TroopsAcademyNumber
            this.UpdateKings(result, kingResult, attack.SenderId, defenderId, defenderAcademyTroopsLoss);

            if (result == AttackResult.AttackerWin || result == AttackResult.AttackerWinGot)
            {
                // Neutral king has no connexity problem !
                if (!defender.IsNeutral())
                {
                    this._queueProvider.PutConnexityBreakPoint(new ConnexityBreakpoint(attack.Destination, defenderId));
                }

                // If attacker can claim the square, verify if the new square is near the kingdom of the attacker i.e. if attacker can own the square
                if (result == AttackResult.AttackerWinGot && !this._connexityProvider.ConnexityAnnexSquare(attack.Destination))
                {
                    // Attacker cannot own the square because of connexity
                    result = AttackResult.AttackerWin;
                }
            }

            this.LogAttackResult(result, kingResult, attack, defenderId);

            // TODO : it's not very atomic and it can be a problem
            if (kingResult == KingAttackResult.KingDie)
            {
                string oldOpenId = null;

                // The king is dead, pass its OpenId to null (act nearly as a neutral king)
                var oldDefenderKing = this._gameProvider.UpdateKing(
                    defenderId,
                    king =>
                    {
                        king.UpdateAcademyTroops();
                        king.AcademyTroops = 0;
                        oldOpenId = king.OpenId;
                        king.OpenId = null;

                        return king;
                    });

                var user = this._userProvider.GetUserByOpenId(oldOpenId);
                var newDefenderKing = _userStarter.CreateNewUser(user);

                // Inform the user that its previous king was dead.
                this._logProvider.StoreLog(new Log(newDefenderKing.Id, attack.SenderId, LogKind.AttackedKingLoss, attack.Destination));
            }
        }

        /// <summary>
        /// This function is indirectly used to perform the atomic update.
        /// WARNING : If attacker can claim the square, he gets the square even if there is a problem of connexity.
        /// </summary>
        /// <param name="square">The previous square - can be updated</param>
        /// <param name="attack">The attack</param>
        /// <param name="defender">The king who has the square - AcademyTroops must have been updated and will not be modified.
        /// If the defender if NeutralId, the king should just contains AcademyTroops=0.</param>
        /// <param name="result">The result of the attack</param>
        /// <param name="kingResult">The effect of the attack on the eventual king</param>
        /// <param name="defenderAcademyTroopsLoss">The number of academy troops loss by defender</param>
        /// <returns>The new square</returns>
        private static Square UpdateSquareCallback(Square square, Attack attack, King defender, out AttackResult result, out KingAttackResult kingResult, out int defenderAcademyTroopsLoss)
        {
            var attackerId = attack.SenderId;
            defenderAcademyTroopsLoss = 0;
            kingResult = square.HasKing ? KingAttackResult.KingWin : KingAttackResult.NoKing;

            if (attackerId == square.KingId)
            {
                square.Troops += attack.TroopsNumber;
                result = AttackResult.IncreaseTroops;
            }
            else
            {
                if (attack.TroopsNumber <= square.Troops)
                {
                    square.Troops -= attack.TroopsNumber;
                    result = AttackResult.DefenderWin;
                }
                else if (square.HasKing && attack.TroopsNumber <= defender.AcademyTroops + square.Troops)
                {
                    attack.TroopsNumber -= square.Troops;
                    square.Troops = 0;
                    defenderAcademyTroopsLoss = attack.TroopsNumber;
                    result = AttackResult.DefenderWin;
                }
                else
                {
                    square.Troops = attack.TroopsNumber - square.Troops;

                    if (square.HasKing)
                    {
                        square.Troops -= defender.AcademyTroops;
                        square.HasKing = false;
                        kingResult = KingAttackResult.KingDie;
                    }

                    if (square.Troops >= GlobalParameters.MinimumTroopsToClaimSquare)
                    {
                        square.KingId = attackerId;
                        result = AttackResult.AttackerWinGot;
                    }
                    else
                    {
                        square.KingId = King.NeutralKingId;
                        result = AttackResult.AttackerWin;
                    }
                }
            }

            return square;
        }

        /// <summary>
        /// Log attack result (used by ResolveAttack)
        /// Do not log KingDie because in this case, the log should contain the id of the new king.
        /// </summary>
        /// <param name="result">The result of the attack (provided by UpdateSquareCallback)</param>
        /// <param name="kingResult">The result of the attack on the king (provided by UpdateSquareCallback)</param>
        /// <param name="attack">The attack</param>
        /// <param name="defenderId">The id of the defender (provided by UpdateSquareCallback). You should provide 0 if the king is neutral</param>
        private void LogAttackResult(AttackResult result, KingAttackResult kingResult, Attack attack, long defenderId)
        {
            long attackerId = attack.SenderId;

            switch (result)
            {
                case AttackResult.IncreaseTroops:
                    this._logProvider.StoreLog(new Log(attackerId, attackerId, LogKind.IncreaseTroops, attack.Destination));
                    break;
                case AttackResult.DefenderWin:
                    this._logProvider.StoreLog(new Log(attackerId, defenderId, LogKind.AttackingLoss, attack.Destination));

                    if (kingResult == KingAttackResult.KingWin)
                    {
                        this._logProvider.StoreLog(new Log(attackerId, defenderId, LogKind.KingNotKilled, attack.Destination));
                    }

                    if (defenderId != King.NeutralKingId)
                    {
                        this._logProvider.StoreLog(new Log(defenderId, attackerId, LogKind.AttackedWin, attack.Destination));

                        if (kingResult == KingAttackResult.KingWin)
                        {
                            this._logProvider.StoreLog(new Log(defenderId, attackerId, LogKind.AttackedKingWin, attack.Destination));
                        }
                    }

                    break;
                case AttackResult.AttackerWin:
                case AttackResult.AttackerWinGot:
                    this._logProvider.StoreLog(
                        new Log(
                            attackerId,
                            defenderId,
                            result == AttackResult.AttackerWinGot ? LogKind.AttackingWinGot : LogKind.AttackingWin,
                            attack.Destination));

                    if (kingResult == KingAttackResult.KingDie)
                    {
                        this._logProvider.StoreLog(new Log(attackerId, defenderId, LogKind.KingKilled, attack.Destination));
                    }

                    if (defenderId != King.NeutralKingId)
                    {
                        this._logProvider.StoreLog(new Log(defenderId, attackerId, LogKind.AttackedLoss, attack.Destination));
                    }

                    break;
                case AttackResult.None:
                    throw new InvalidOperationException();
            }
        }

        /// <summary>
        /// Update attacker and defender kings :
        ///  * squares number of the two kings (attacker and defender) if needed (i.e. if the square has changed owners)
        ///  * academy troops of the defender. If the loss is bigger than the number of academy troops : put 0
        /// (this case can arrive when the user send troops during the attack solving process -> very rare)
        ///  * if defender king is died, do not modify anything
        ///  * TODO : SquareRectangle
        /// (used by ResolveAttack)
        /// </summary>
        /// <param name="result">The result of the attack (provided by UpdateSquareCallback)</param>
        /// <param name="kingResult">The result of the attack on the king (provided by UpdateSquareCallback)</param>
        /// <param name="attackerId">The id of the attacker king</param>
        /// <param name="defenderId">The if of the defender king (provided by UpdateSquareCallback</param>
        /// <param name="defenderAcademyTroopsLoss">Number of academy troops loss by defender</param>
        private void UpdateKings(AttackResult result, KingAttackResult kingResult, long attackerId, long defenderId, int defenderAcademyTroopsLoss)
        {
            if (result == AttackResult.AttackerWinGot)
            {
                int defenderSquaresNumber = 0;

                if (kingResult == KingAttackResult.KingDie && defenderId != King.NeutralKingId)
                {
                    defenderSquaresNumber = _gameProvider.GetKingById(defenderId).SquaresNumber;
                }

                this._gameProvider.UpdateKing(
                    attackerId,
                    king =>
                    {
                        king.UpdateAcademyTroops();
                        if (kingResult == KingAttackResult.KingDie)
                        {
                            // Bonus of academy troops when you kill a king
                            king.AcademyTroops += (GlobalParameters.TroopsPerDefenderSquare * defenderSquaresNumber) + GlobalParameters.TroopsKillKing;
                        }

                        king.SquaresNumber++;
                        return king;
                    });
            }

            if (defenderId != King.NeutralKingId && kingResult != KingAttackResult.KingDie)
            {
                this._gameProvider.UpdateKing(
                    defenderId,
                    king =>
                    {
                        king.UpdateAcademyTroops();
                        king.AcademyTroops = king.AcademyTroops >= defenderAcademyTroopsLoss
                                                 ? king.AcademyTroops - defenderAcademyTroopsLoss
                                                 : 0;
                        if (result == AttackResult.AttackerWin || result == AttackResult.AttackerWinGot)
                        {
                            king.SquaresNumber--;
                        }

                        return king;
                    });
            }
        }
    }
}